home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / AppInstall / Menu.py < prev    next >
Encoding:
Python Source  |  2009-04-30  |  16.8 KB  |  454 lines

  1. # (c) 2005-2007 Canonical - GPL
  2. # (c) 2006-2007 Sebastian Heinlein
  3. #
  4. # Authors:
  5. #  Michael Vogt
  6. #  Sebastian Heinlein
  7. #
  8.  
  9. import gtk
  10. import gtk.gdk
  11. import gobject
  12. import xdg.Menu
  13. import sys
  14. import os
  15. import gettext
  16. import gst
  17.  
  18. from warnings import warn
  19. from gettext import gettext as _
  20.  
  21. from Util import *
  22.  
  23. from CoreMenu import *
  24.  
  25. # possible filter states for the application list - match the layout in
  26. # data/gnome-app-install.schemas.in
  27. ( SHOW_ALL,
  28.   SHOW_ONLY_FREE,
  29.   UNUSED_1,
  30.   SHOW_ONLY_SUPPORTED,
  31.   SHOW_ONLY_THIRD_PARTY,
  32.   UNUSED_2,
  33.   SHOW_ONLY_INSTALLED,
  34.   SHOW_ONLY_MAIN,
  35.   SHOW_ONLY_PROPRIETARY,
  36.  ) = range(9)
  37.  
  38. PIMP_APPS = ["gstreamer0.10-plugins-ugly",
  39.              "gstreamer0.10-ffmpeg",
  40.              "sun-java5-plugin",
  41.              "flashplugin-nonfree",
  42.              "ubuntu-restricted-extras"]
  43.  
  44. class NullActivationStyleForMenu:
  45.     # See class ActivationStyle in gnome-app-install; that class
  46.     # would do just fine here except that it's upside-down from a
  47.     # layering POV.
  48.     def __init__(self): 
  49.         self.selectFilter = None
  50.         self.menuFilter = None
  51.     def isApproved(self, component, pkgname): return True
  52.     def menuCacheName(self): return "menu.p"
  53.  
  54. class ApplicationMenu(CoreApplicationMenu):
  55.     """ this represents the application menu, the interessting bits are:
  56.         - store that can be attached to a TreeView
  57.         - pkg_to_app a dictionary that maps the apt pkgname to the application
  58.                      items
  59.     """
  60.  
  61.     debug = 0
  62.  
  63.     def __init__(self, datadir, cachedir, cache,
  64.                  treeview_packages, progress,
  65.                  filter=SHOW_ONLY_SUPPORTED, dontPopulate=False,
  66.                  activation_style=NullActivationStyleForMenu()):
  67.         CoreApplicationMenu.__init__(self, datadir)
  68.         self.cache = cache
  69.         self.treeview_packages = treeview_packages
  70.         self.activationStyle = activation_style
  71.         
  72.         # icon theme
  73.         self.icons = gtk.icon_theme_get_default()
  74.         self.icons.append_search_path(os.path.join(datadir, "icons"))
  75.  
  76.         # search
  77.         self.searchTerms = []
  78.         
  79.         # properties for the view
  80.         if self.activationStyle.menuFilter is not None:
  81.             self.filter = self.activationStyle.menuFilter
  82.         else:
  83.             self.filter = filter
  84.  
  85.         # the categories 
  86.         self.real_categories_store = gtk.ListStore(gobject.TYPE_STRING,
  87.                                                    gobject.TYPE_PYOBJECT)
  88.  
  89.         if dontPopulate:
  90.             return
  91.  
  92.         # populate the tree
  93.         # use cached self.pickle (should be renamed to self.categories)
  94.         # and cache self.pkgs_to_app
  95.         cacheLoaded = False
  96.         cname = activation_style.menuCacheName()
  97.         if cachedir is not None and os.path.exists("%s/%s" % (cachedir,cname)):
  98.             progress.label_action.set_label(_("Loading cache..."))
  99.             import cPickle
  100.             try:
  101.                 self.pickle = cPickle.load(open("%s/%s" % (cachedir,cname)))
  102.                 cacheLoaded = True
  103.             except:
  104.                 pass
  105.  
  106.         if not cacheLoaded:
  107.             progress.label_action.set_label(_("Collecting application data..."))
  108.             self.desktopEntriesSeen.clear()
  109.             menu = xdg.Menu.parse(os.path.join(self.menudir, "applications.menu"))
  110.             self._populateFromEntry(menu)
  111.             
  112.         # refresh based on the pickled information
  113.         self.refresh(progress)
  114.         self.store = self.real_categories_store
  115.  
  116.     def get_categories_store(self):
  117.         return self.real_categories_store
  118.  
  119.     # helpers
  120.     def _refilter(self, model=None):
  121.         # we need to disconnect the model from the view when we
  122.         # do a refilter, otherwise we get random crashes in the search
  123.         # (to reproduce:
  124.         #  1. open "accessability" 2. unselect "show unsupported"
  125.         #  3. search for "apt" 4. turn "show unsupported" on/off -> BOOM
  126.         if not model:
  127.             model = self.treeview_packages.get_model()
  128.  
  129.         # save the cursor position (or rather, the name of the app selected)
  130.         name = None
  131.         (path, colum) = self.treeview_packages.get_cursor()
  132.         if path:
  133.             try:
  134.                 name = model.get_value(model.get_iter(path), COL_NAME)
  135.             except ValueError, e:
  136.                 # gtk allows a cursor on position (0,) for a empty treeview
  137.                 # but errors if that path from get_cursor() is used in
  138.                 # get_value()
  139.                 pass
  140.             #print "found: %s (%s) " % (name, path)
  141.  
  142.         # this is the actual refiltering
  143.         self.treeview_packages.set_model(None)
  144.         if model != None:
  145.             model.get_model().refilter()
  146.         self.treeview_packages.set_model(model)
  147.  
  148.         # redo the cursor
  149.         if name != None:
  150.             for it in iterate_list_store(model, model.get_iter_first()):
  151.                 aname = model.get_value(it, COL_NAME)
  152.                 if name == aname:
  153.                     #print "selecting: %s (%s)" % (name, model.get_path(it))
  154.                     #self.treeview_packages.expand_to_path(model.get_path(it))
  155.                     self.treeview_packages.set_cursor(model.get_path(it))
  156.                     return
  157.         elif len(model) > 0:
  158.             self.treeview_packages.set_cursor(0)
  159.  
  160.     def _name_sort_func(self, model, iter1, iter2):
  161.         """
  162.         Sort by name, honor special craziness
  163.         """
  164.         item1 = model.get_value(iter1, COL_ITEM)
  165.         item2 = model.get_value(iter2, COL_ITEM)
  166.         if item1 == None or item2 == None:
  167.             return 0
  168.         # check if we want always on top
  169.         # - we only want it on top if the category is not "All"
  170.         # - if the item has the onTop property
  171.         cat = model.get_data("category") 
  172.         if (cat and cat.name != self.all_category_name and
  173.             hasattr(item1,"onTop") and hasattr(item2,"onTop")):
  174.             # if both have the onTop property, fall through and
  175.             # do normal name sorting
  176.             if not (item1.onTop and item2.onTop):
  177.                 if item1.onTop: return -1
  178.                 elif item2.onTop: return 1
  179.         # no onTop property
  180.         name1 = model.get_value(iter1, COL_NAME)
  181.         name2 = model.get_value(iter2, COL_NAME)
  182.         if name1 < name2: return -1
  183.         elif name1 > name2: return 1
  184.         else: return 0
  185.  
  186.     def _ranking_sort_func(self, model, iter1, iter2):
  187.         """
  188.         Sort by the search result rank
  189.         """
  190.         #print "_sort_func()"
  191.         item1 = model.get_value(iter1, COL_ITEM)
  192.         item2 = model.get_value(iter2, COL_ITEM)
  193.         if item1 == None or item2 == None:
  194.             return 0
  195.         if item1.rank < item2.rank: return 1
  196.         elif item1.rank > item2.rank: return -1
  197.         else: return 0
  198.  
  199.     def _visible_filter(self, model, iter):
  200.         item = model.get_value(iter, COL_ITEM)
  201.         #print "_visible_filter: ", item
  202.         if item:
  203.             # check for the various view settings
  204.             if not self.activationStyle.isApproved(
  205.                item.component, item.pkgname):
  206.                 return False
  207.             if self.filter == SHOW_ONLY_MAIN and item.component != "main":
  208.                 return False
  209.             if self.filter == SHOW_ONLY_SUPPORTED and item.supported != True:
  210.                 return False
  211.             if self.filter == SHOW_ONLY_FREE and item.free == False:
  212.                 return False
  213.             if self.filter == SHOW_ONLY_PROPRIETARY and item.free == True:
  214.                 return False
  215.             if self.filter == SHOW_ONLY_THIRD_PARTY and item.thirdparty != True:
  216.                 return False
  217.             if self.filter == SHOW_ONLY_INSTALLED and not \
  218.                self.itemIsInstalled(item):
  219.                 return False
  220.             # Allow to only show a subset by the activation filter
  221.             if (self.activationStyle.selectFilter is not None and
  222.                 not self._activationStyleFilter(item)):
  223.                 return False
  224.             # if we search, do the ranking updates 
  225.             if len(self.searchTerms) > 0:
  226.                 rank = self._filterAndRank(item)
  227.                 if rank == None:
  228.                     return False
  229.                 else:
  230.                     item.rank = rank
  231.         return True
  232.  
  233.     def _filterAndRank(self, item):
  234.         """
  235.         Watch out, Google!
  236.         """
  237.         trigger = ""
  238.         rank = 100 * item.popcon / self.popcon_max
  239.  
  240.         # the normal case
  241.         for term in self.searchTerms:
  242.             hit = False
  243.             if term == item.name.lower() or \
  244.                term == item.pkgname.lower():
  245.                 rank += 100
  246.                 hit = True
  247.             if term in item.name.lower():
  248.                 rank += 30
  249.                 trigger += " name"
  250.                 hit = True
  251.             if term in item.pkgname.lower():
  252.                 rank += 30
  253.                 trigger += " pkg_name"
  254.                 hit = True
  255.             if self._mimeMatch(item, term, fuzzy=True):
  256.                 rank += 25
  257.                 trigger += " mime"
  258.                 hit = True
  259.             if self._codecMatch(item, term, fuzzy=True):
  260.                 rank += 15
  261.                 trigger += " codec"
  262.                 hit = True
  263.             if self.cache.has_key(item.pkgname) and \
  264.                  term in self.cache[item.pkgname].description.lower():
  265.                 rank += 10
  266.                 trigger += " pkg_desc"
  267.                 hit = True
  268.             if hit == False:
  269.                 return None
  270.             if item.pkgname.lower() in PIMP_APPS:
  271.                 rank += 75
  272.         #print "found %s (%s/%s): %s" % (item.name, item.popcon, rank, trigger)
  273.         return rank
  274.  
  275.     def _mimeMatch(self, item, term, fuzzy=False):
  276.         for pattern in item.mime:
  277.             if fuzzy and term in pattern:
  278.                 return True
  279.             elif not fuzzy and pattern == term:
  280.                 return True
  281.         return False
  282.  
  283.     def _codecMatch(self, item, term, fuzzy=False):
  284.         #print "_codecMatch for pkg '%s' (term: %s) " % (item.pkgname, term)
  285.         if ":" in term:
  286.             term = term.split(":")[1]
  287.         search_cap = gst.caps_from_string(term)
  288.         #print "search_cap: ", search_cap
  289.         for codec in item.codecs:
  290.             if ":" in codec:
  291.                 codec = codec.split(":")[1]
  292.             if fuzzy and term in codec:
  293.                 return True
  294.             else:
  295.                 cap = gst.caps_from_string(codec)
  296.                 #print "codec: ",codec
  297.                 #print "cap: ",cap
  298.                 if (cap and search_cap and
  299.                     (search_cap & cap)):
  300.                     return True
  301.         return False
  302.  
  303.     def _activationStyleFilter(self, item):
  304.         #print "_activationStyleFilter(): ",item
  305.         filter = self.activationStyle.selectFilter(self)
  306.         if (self.activationStyle.isInstallerOnly and 
  307.             self.itemIsInstalled(item)):
  308.             return False
  309.         for term in self.activationStyle.searchTerms():
  310.             if filter(item, term):
  311.                 return True
  312.         return False
  313.  
  314.     def doMimeSearch(self, mime_type, fuzzy=False):
  315.         res = set()
  316.         model = self.real_categories_store.get_value(self.all_category_iter, COL_CAT_ITEM).all_applications
  317.         for it in iterate_list_store(model, model.get_iter_first()):
  318.             item = model.get_value(it, COL_ITEM)
  319.             for re_pattern in item.mime:
  320.                 # mvo: we get a list of regexp from
  321.                 # pyxdg.DesktopEntry.getMimeType, but it does not
  322.                 # use any special pattern at all, so we use the plain
  323.                 # pattern (e.g. text/html, audio/mp3 here)
  324.                 pattern = re_pattern.pattern
  325.                 if fuzzy and mime_type in pattern:
  326.                     res.add(item)
  327.                 elif not fuzzy and re_pattern.match(mime_type):
  328.                     res.add(item)
  329.         return res
  330.  
  331.     def refreshAfterCacheChange(self, progress):
  332.         #print "refreshAfterCacheChange"
  333.         # FIXME: progress information here?
  334.         for cat in self.pickle:
  335.             for item in self.pickle[cat]:
  336.                 #print item
  337.                 item.toInstall = (self.cache.has_key(item.pkgname) and
  338.                                   self.cache[item.pkgname].isInstalled)
  339.  
  340.     def refresh(self, progress):
  341.         self.real_categories_store.clear()
  342.         progress.subOp = _("Loading applications...")
  343.         progress.update(0)
  344.         # add "All" category
  345.         self.all_category_iter = self.real_categories_store.append()
  346.         self.all_category_name = "<b>%s</b>" % _("All")
  347.         item = Category(self, self.all_category_name, "distributor-logo")
  348.         self.initListStores(item, self)
  349.         self.real_categories_store.set(self.all_category_iter,
  350.                                        COL_CAT_NAME, "<b>%s</b>" % _("All"),
  351.                                        COL_CAT_ITEM, item)
  352.  
  353.         # now go for the categories
  354.         i=0
  355.         lenx=len(self.pickle.keys())
  356.         keys = self.pickle.keys()
  357.         keys.sort(cmp=lambda x,y: cmp(x.name.lower(), y.name.lower()))
  358.         for category in keys:
  359.             progress.subOp = _("Loading %s...")% category.name
  360.             self.initListStores(category, self)
  361.             self.real_categories_store.set(self.real_categories_store.append(),
  362.                                            COL_CAT_NAME, category.name,
  363.                                            COL_CAT_ITEM, category,)
  364.             i += 1
  365.             progress.update(i/float(lenx)*100.0)
  366.             for item in self.pickle[category]:
  367.                 # add to category
  368.                 category.all_applications.append([item.name,
  369.                                                   item,
  370.                                                   item.popcon])
  371.                 # add to all
  372.                 store = self.real_categories_store.get_value(self.all_category_iter, COL_ITEM).all_applications
  373.                 store.append([item.name, item, item.popcon])
  374.  
  375.                 # do the popcon_max calculation
  376.                 if item.popcon > self.popcon_max:
  377.                     self.popcon_max = item.popcon
  378.  
  379.                 # populate the pkg_to_app data structure
  380.                 pkgname = item.pkgname
  381.                 if not self.pkg_to_app.has_key(pkgname):
  382.                     self.pkg_to_app[pkgname] = set()
  383.                 self.pkg_to_app[pkgname].add(item)
  384.         # now update the cache dependant part
  385.         self.refreshAfterCacheChange(progress)
  386.  
  387.     def itemAvailable(self, item):
  388.         """ returns True if the item is available in the current
  389.             apt cache """
  390.         return self.cache.has_key(item.pkgname)
  391.     def itemIsInstalled(self, item):
  392.         """ returns True if the item is currently installed """
  393.         return (self.cache.has_key(item.pkgname) and
  394.                 self.cache[item.pkgname].isInstalled)
  395.  
  396.     def initListStores(self, category, parent):
  397.         # if that category has applications, add them to the
  398.         # store here
  399.         category.all_applications = gtk.ListStore(gobject.TYPE_STRING,
  400.                                                   gobject.TYPE_PYOBJECT,
  401.                                                   gobject.TYPE_INT)
  402.         # set the visible filter
  403.         category.filtered_applications = category.all_applications.filter_new()
  404.         category.filtered_applications.set_visible_func(parent._visible_filter)
  405.         category.filtered_applications.set_data("category",category)
  406.         # set the model sort all applications
  407.         category.applications = gtk.TreeModelSort(category.filtered_applications)
  408.         category.applications.set_data("category",category)
  409.  
  410.     def getChanges(self, get_paths=False):
  411.         """ return the selected changes in the tree
  412.             TODO: what is get_paths?
  413.         """
  414.         to_inst = set()
  415.         to_rm = set()
  416.         for (name, item) in self.store:
  417.             for (name,item,popcon) in item.all_applications:
  418.                 if self.itemIsInstalled(item) and not item.toInstall:
  419.                     to_rm.add(item)
  420.                 if not self.itemIsInstalled(item) and item.toInstall:
  421.                     to_inst.add(item)
  422.         #print "to_add: %s" % to_inst
  423.         #print "to_rm: %s" % to_rm
  424.         return (to_inst, to_rm)
  425.         
  426.     def isChanged(self):
  427.         """ check if there are changes at all """
  428.         for (cat_name, cat)  in self.store:
  429.             for (name,item,popcon) in cat.all_applications:
  430.                 if item.toInstall != self.itemIsInstalled(item):
  431.                     return True
  432.         return False
  433.  
  434.     def _dbg(self, level, msg):
  435.         """Write debugging output to sys.stderr."""
  436.         if level <= self.debug:
  437.             print >> sys.stderr, msg
  438.  
  439.  
  440. if __name__ == "__main__":
  441.     print "testing the menu"
  442.  
  443.     desktopdir = "/usr/share/app-install"
  444.     from Util import MyCache
  445.     cache = MyCache()
  446.     treeview = gtk.TreeView()
  447.     menu = ApplicationMenu(desktopdir, cache, treeview, treeview, apt.progress.OpProgress())
  448.     #matches = menu.doMimeSearch("mp3",fuzzy=True)
  449.     #print matches
  450.     #matches = menu.doMimeSearch("audio/mp3")
  451.     #print matches
  452.  
  453.     
  454.